using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using SPTraceView.Model;

namespace SPTraceView.Model
{
    internal sealed class TraceEventArgs : EventArgs
    {
        internal readonly ETW.EVENT_TRACE RawTraceData;
        internal readonly ULS.ULSTraceHeader Event;
        internal readonly ULS.TraceSeverity Severity;
        internal readonly MultipleLinesEvent MultiLinesEvent;
        internal readonly Int64 TimeCorrection;

        public TraceEventArgs(ETW.EVENT_TRACE eventTrace, ULS.ULSTraceHeader evt, ULS.TraceSeverity severity, Int64 timeCorrection)
        {
            RawTraceData = eventTrace;
            Event = evt;
            Severity = severity;
            MultiLinesEvent = null;
            TimeCorrection = timeCorrection;
        }

        public TraceEventArgs(ETW.EVENT_TRACE eventTrace, MultipleLinesEvent evt, ULS.TraceSeverity severity, Int64 timeCorrection)
        {
            RawTraceData = eventTrace;
            MultiLinesEvent = evt;
            Severity = severity;
            TimeCorrection = timeCorrection;
        }
    }

    internal sealed class TraceConsumer
    {
        private ETW.EVENT_TRACE_LOGFILE m_traceLogFile;
        //private ETW.EVENT_TRACE_PROPERTIES_WITH_NAMES m_traceProps;
        private ulong m_UlsTraceHandle = ETW.INVALID_HANDLE_VALUE;

        private long m_LostEvents;
        private Int64 m_TimerCorrection;

        private TraceSettings m_TraceSettings;
        private object m_SyncRoot = new object();

        private List<MultipleLinesEvent> m_MultiLineEvents = new List<MultipleLinesEvent>();

        public delegate void TraceEventCallback(TraceEventArgs e);
        private TraceEventCallback m_Callback;

        private Thread m_TraceWorkerThread = null;
        internal uint m_ErrorAttaching = 0;


        public TraceConsumer(TraceEventCallback callback, TraceSettings settings)
        {
            m_Callback = callback;
            m_TraceSettings = new TraceSettings(true);

            UpdateTraceSettings(settings);
        }

        public void UpdateTraceSettings(TraceSettings settings)
        {
            Debug.WriteLine("TraceConsumer.UpdateTraceSettings");

            lock (m_SyncRoot)
            {
                m_TraceSettings.CopyFrom(settings);
            }
        }

        public int Run()
        {
            Debug.WriteLine("TraceConsumer.Run");

            m_traceLogFile = new ETW.EVENT_TRACE_LOGFILE();
            m_traceLogFile.LoggerName = ULS.WSS_TRACE_SESSION_NAME;// SP_LOG_TRACE_SESSSION_NAME;
            m_traceLogFile.LogFileMode = ETW.EVENT_TRACE_REAL_TIME_MODE;
            m_traceLogFile.BufferCallback = new ETW.EVENT_TRACE_BUFFER_CALLBACK(UlsBufferCallback);
            m_traceLogFile.EventCallback = new ETW.EVENT_CALLBACK(UlsEventCallback);

            m_UlsTraceHandle = ETW.OpenTrace(ref m_traceLogFile);

            if (m_UlsTraceHandle == ETW.INVALID_HANDLE_VALUE)
            {
                int lastError = Marshal.GetLastWin32Error();
                Trace.WriteLine("OpenTrace() failed with error code " + lastError.ToString(), "SPTraceView");
                return lastError;
            }
            else
            {
                Trace.WriteLine("Tracing session successfully initiated.", "SPTraceView");
            }

            EnableEventFiring();

            m_TraceWorkerThread = new Thread(new ThreadStart(RunUlsTrace_BlockingThread));
            m_TraceWorkerThread.SetApartmentState(ApartmentState.STA);
            m_TraceWorkerThread.Start();


            Thread.Sleep(1000);

            return (int)m_ErrorAttaching;
        }


        public bool Stop()
        {
            Debug.WriteLine("TraceConsumer.Stop");

            if (m_UlsTraceHandle != ETW.INVALID_HANDLE_VALUE)
            {
                Trace.WriteLine(string.Format("Tracing session stop request received. There are {0} undelivered multiline events.", m_MultiLineEvents.Count), "SPTraceView");

                uint res = ETW.CloseTrace(m_UlsTraceHandle);

                if (res == ETW.ERROR_CTX_CLOSE_PENDING)
                {
                    Trace.WriteLine("Tracing session with handle " + m_UlsTraceHandle.ToString() + " is pending to be closed.", "SPTraceView");
                }
                else if (res == ETW.ERROR_BUSY)
                {
                    //Trace.WriteLine("Trace with handle " + m_UlsTraceHandle.ToString() + " is bussy and did not respond. It will be closed later.", "SPTraceView");
                    Trace.WriteLine("Waiting for the tracing session to be closed ...", "SPTraceView");
                }
                else if (res != 0)
                {
                    Trace.WriteLine("Error closing tracing session: " + res.ToString(), "SPTraceView");
                }

                // Stop the callbacks
                m_Callback = null;
            }

            if (m_TraceWorkerThread != null)
            {
                if (m_TraceWorkerThread.IsAlive)
                {
                    m_TraceWorkerThread.Abort();
                    m_TraceWorkerThread.Join();
                }

                m_TraceWorkerThread = null;
            }

            m_UlsTraceHandle = ETW.INVALID_HANDLE_VALUE;

            m_TraceSettings.Stop();

            return true;
        }

        public delegate void TraceAsyncMethod();

        private void RunUlsTrace_BlockingThread()
        {
            ulong[] handleArray = new ulong[] { m_UlsTraceHandle };
            
            try
            {
                Trace.WriteLine("Listening for trace events ...", "SPTraceView");
                uint res = ETW.ProcessTrace(handleArray, (uint)handleArray.Length, IntPtr.Zero, IntPtr.Zero);

                if (res != 0)
                {
                    m_ErrorAttaching = res;
                    m_UlsTraceHandle = ETW.INVALID_HANDLE_VALUE;

                    if (res == 5)
                        Trace.WriteLine("Access Denied! You must be administrator to listen for trace events.", "SPTraceView");

                    else if (res == 4201)
                        Trace.WriteLine("Make sure the 'Windows SharePoint Services Tracing' service is running.", "SPTraceView");

                    Trace.WriteLine("Error connecting to trace session. The error code is: " + res.ToString(), "SPTraceView");
                }
            }
            finally
            {
                Thread.Sleep(1000);

                if (m_UlsTraceHandle != ETW.INVALID_HANDLE_VALUE)
                {
                    uint res = ETW.CloseTrace(m_UlsTraceHandle);

                    if (res != 0)
                        Trace.WriteLine("CloseTrace() failed with error code " + res.ToString() + " when trying to close the trace.", "SPTraceView");
                    else
                        Trace.WriteLine("Tracing session closed.", "SPTraceView");
                }
                else
                    Trace.WriteLine("Tracing session already closed.", "SPTraceView");
            }
        }

        private long TimerCorrection
        {
            set
            {
                Interlocked.Exchange(ref m_TimerCorrection, value);
            }
        }

        private bool m_EventFiringEnabled = false;

        public void EnableEventFiring()
        {
            m_EventFiringEnabled = true;
        }

        public void DisableEventFiring()
        {
            m_EventFiringEnabled = false;
        }

        private uint UlsBufferCallback(ref ETW.EVENT_TRACE_LOGFILE logfile)
        {
            try
            {
                long timeCorrection = logfile.CurrentTime - DateTime.Now.ToFileTime();
                TimerCorrection = timeCorrection;
                m_LostEvents += logfile.EventsLost;

                Debug.WriteLine(string.Format("TimerCorrection: {0}; Lost Events: {1}", timeCorrection, m_LostEvents));
            }
            catch
            { }

            return 1;
        }

        private void UlsEventCallback(ref ETW.EVENT_TRACE eventTrace)
        {
            Debug.WriteLine("UlsEventCallback", "SPTraceView");

            if (!m_EventFiringEnabled)
                return;

            bool traceDataInitialized = false;
            bool shouldBeIncluded = false;
            ULS.ULSTraceHeader traceData = new ULS.ULSTraceHeader();
            ULS.TraceSeverity severity = ULS.TraceSeverity.Unassigned;

            try
            {
                traceData = (ULS.ULSTraceHeader)Marshal.PtrToStructure(eventTrace.MofData, typeof(ULS.ULSTraceHeader));
                traceDataInitialized = true;

                if (Enum.IsDefined(typeof(ULS.TraceSeverity), (int)eventTrace.Header.Level))
                    severity = (ULS.TraceSeverity)Enum.ToObject(typeof(ULS.TraceSeverity), (int)eventTrace.Header.Level);

                lock (m_SyncRoot)
                {
                   shouldBeIncluded = m_TraceSettings.IsMatch(traceData, severity);
                }

            }
            catch (Exception ex)
            {
                StringBuilder errInfo = new StringBuilder("Error processing ULS event: ");

                while (ex != null)
                {
                    errInfo.Append(ex.GetType().ToString());
                    errInfo.Append(" : ");
                    errInfo.Append(ex.Message);
                    errInfo.Append("\r\n");
                    errInfo.Append(ex.StackTrace);
                    errInfo.Append("\r\n----------------------------------------------------------------------------------------");

                    ex = ex.InnerException;
                }

                if (traceDataInitialized)
                {
                    errInfo.Append("\r\n\r\n");
                    errInfo.Append("ExeName: ");
                    errInfo.Append(traceData.ExeName);
                    errInfo.Append("\r\n");
                    errInfo.Append("Product: ");
                    errInfo.Append(traceData.Product);
                    errInfo.Append("\r\n");
                    errInfo.Append("Category: ");
                    errInfo.Append(traceData.Category);
                    errInfo.Append("\r\n");
                    errInfo.Append("Message: ");
                    errInfo.Append(traceData.Message);
                    errInfo.Append("\r\n");
                }

                Trace.WriteLine(errInfo.ToString(), "SPTraceView");
            }

            if (shouldBeIncluded)
            {
                if (traceData.Flags == ULS.TraceFlags.TRACE_FLAG_ID_AS_ASCII)
                {
                    // The meesage is a full message (one only)
                    if (m_Callback != null)
                        m_Callback.BeginInvoke(new TraceEventArgs(eventTrace, traceData, severity, m_TimerCorrection), null, null);

                    //Debug.WriteLine("Callback.BeginInvoke called", "SPTraceView");
                }
                else if (
                    traceData.Flags == ULS.TraceFlags.TRACE_FLAG_START ||
                    traceData.Flags == ULS.TraceFlags.TRACE_FLAG_UNKNOWN_5)
                {
                    MultipleLinesEvent evt = new MultipleLinesEvent(traceData, eventTrace.Header);
                    m_MultiLineEvents.Add(evt);

                    //Debug.WriteLine("MultipleLinesEvent added.", "SPTraceView");
                }
                else if (traceData.Flags == ULS.TraceFlags.TRACE_FLAG_MIDDLE)
                {
                    foreach (MultipleLinesEvent evt in m_MultiLineEvents)
                    {
                        if (evt.ProcessId == eventTrace.Header.ProcessId &&
                            evt.EventId == traceData.Id)
                        {
                            evt.AddEventPart(traceData);
                            //Debug.WriteLine("MultipleLinesEvent updated.", "SPTraceView");
                            break;
                        }
                    }
                }
                else if (
                       traceData.Flags == ULS.TraceFlags.TRACE_FLAG_END ||
                       traceData.Flags == ULS.TraceFlags.TRACE_FLAG_UNKNOWN_6)
                {
                    foreach (MultipleLinesEvent evt in m_MultiLineEvents)
                    {
                        if (evt.ProcessId == eventTrace.Header.ProcessId &&
                            evt.EventId == traceData.Id)
                        {
                            evt.AddEventPart(traceData);
                            m_MultiLineEvents.Remove(evt);

                            if (m_Callback != null)
                                m_Callback.BeginInvoke(new TraceEventArgs(eventTrace, evt, severity, m_TimerCorrection), null, null);
                            //Debug.WriteLine("MultipleLinesEvent Callback.BeginInvoke called", "SPTraceView");

                            break;
                        }
                    }
                }
                else
                {
                    Debug.WriteLine("Invalid trace flags: " + traceData.Flags.ToString(), "SPTraceView");
                }
            }
            else
            {
                Debug.WriteLine("EXCLUDED", "SPTraceView");
            }
        }
    }

    internal sealed class MultipleLinesEvent
    {
        internal readonly uint ProcessId;
        internal readonly uint EventId;

        private ULS.ULSTraceHeader m_Event1;
        private ULS.ULSTraceHeader m_Event2;
        private ULS.ULSTraceHeader m_Event3;
        private ULS.ULSTraceHeader m_Event4;
        private ULS.ULSTraceHeader m_Event5;

        internal ULS.ULSTraceHeader Event1
        {
            get { return m_Event1; }
        }

        internal ULS.ULSTraceHeader Event2
        {
            get { return m_Event2; }
        }

        internal ULS.ULSTraceHeader Event3
        {
            get { return m_Event3; }
        }

        internal ULS.ULSTraceHeader Event4
        {
            get { return m_Event4; }
        }

        internal ULS.ULSTraceHeader Event5
        {
            get { return m_Event5; }
        }

        private int m_CurrentPartId = 1;

        internal int NumberOfParts
        {
            get { return m_CurrentPartId - 1; }
        }

        internal MultipleLinesEvent(ULS.ULSTraceHeader evt, ETW.EVENT_TRACE_HEADER hrd)
        {
            ProcessId = hrd.ProcessId;
            EventId = evt.Id;

            AddEventPart(evt);
        }

        internal void AddEventPart(ULS.ULSTraceHeader evt)
        {
            switch (m_CurrentPartId)
            {
                case 1:
                    m_Event1 = evt;
                    break;

                case 2:
                    m_Event2 = evt;
                    break;

                case 3:
                    m_Event3 = evt;
                    break;

                case 4:
                    m_Event4 = evt;
                    break;

                case 5:
                    m_Event5 = evt;
                    break;

                default:
                    return;
            }

            m_CurrentPartId++;
        }
    }
}
